namespace Nico {
    /**
     * Errors that may occur when working with a {@link Nico.Client}
     */
    public errordomain ClientError {
        UNKNOWN_LOCATION
    }

    /**
     * A generic Client to smoking Servers
     */
    public class Client : GLib.Object {
        private Socket connection;
        private string username;
        private List<Session> sessions;
        private List<string> locations;

        /**
         * A {@link Nico.Resources}-Object that represents which
         * resources the client has available currently.
         */
        public Resources resources {get; private set;}

        private const uint BUFFER_SIZE = 1024;

        /**
         * Creates a new client and lets it connect to the given server
         */
        public Client(string addr, uint16 port, string username) {
            this.resources = new Resources();
            this.username = username;
            var address = new InetAddress.any (SocketFamily.IPV4);
            var inetaddress = new InetSocketAddress (address, port);

            this.connection = new Socket (SocketFamily.IPV4, SocketType.STREAM, SocketProtocol.TCP);
            assert (this.connection != null);

            this.connection.connect (inetaddress);
            stdout.puts ("connected\n");

            try {
                new Thread<int>.try("incominghandler", this.listen);
            } catch (Error e) {
                warning("Could not start thread to handle incoming connections");
            }

        }

        /**
         * Sends a hello-message to the server. This tells the server this
         * client's username and makes the client receive updates when
         * something changes on the server.
         */
        public void send_helo() {
            var helo = new Helo();
            helo.username = this.username;
            size_t len = 0;
            var data = " "+Json.gobject_to_data(helo, out len);
            if (len > BUFFER_SIZE - 1) {
                warning("Could not send helo to server: Message would be too long");
                return;
            }
            var bytes = data.data;
            bytes[0] = Messages.HELO;
            try {
                this.connection.send(bytes);
            } catch (GLib.Error e) {
                message("Error: "+e.message);
                this.connection.close();
            }
        }

        /**
         * Makes the client join the given session
         */
        public void join_session(Session session) {
            var ack = new Ack();
            ack.session_id = session.id;
            ack.resources = this.resources;
            size_t len = 0;
            var data = " "+Json.gobject_to_data(ack, out len);
            if (len > BUFFER_SIZE - 1) {
                warning("Could not acknowledge session %d: Message would be too long", session.id);
                return;
            }
            var bytes = data.data;
            bytes[0] = Messages.ACK;
            try {
                this.connection.send(bytes);
            } catch (GLib.Error e) {
                message("Error: "+e.message);
                this.connection.close();
            }
        }

        /**
         * Makes the client create a session
         */
        public int create_session(DateTime time, string location) {
            var session = new Session();
            lock (this.resources) {
                session.resources = this.resources;
            }
            session.users = {this.username};
            session.datetime = time.to_string();
            lock (this.locations) {
                bool found = false;
                foreach (var loc in this.locations) if (loc == location) found = true;
                if (!found) {
                    throw new ClientError.UNKNOWN_LOCATION("Unknown location: %s", location);
                }
            }

            size_t len = 0;
            var data = " "+Json.gobject_to_data(session, out len);
            if (len > BUFFER_SIZE - 1) {
                warning("Could not create session: Message would be too long");
                return 1;
            }
            var bytes = data.data;
            bytes[0] = Messages.CREATE;
            try {
                this.connection.send(bytes);
            } catch (GLib.Error e) {
                message("Error: "+e.message);
                this.connection.close();
            }
            return 0;
        }

        /**
         * Makes the client reject the given session
         */
        public void reject_session(Session session) {
            var nack = new Nack();
            nack.session_id = session.id;
            size_t len = 0;
            var data = " "+Json.gobject_to_data(nack, out len);
            if (len > BUFFER_SIZE - 1) {
                warning("Could not reject session %d: Message would be too long", session.id);
                return;
            }
            var bytes = data.data;
            bytes[0] = Messages.NACK;
            try {
                this.connection.send(bytes);
            } catch (GLib.Error e) {
                message("Error: "+e.message);
                this.connection.close();
            }
        }

        private int listen() {
            while (!this.connection.is_closed()) {
                var valid = true;
                var msgbuf = new uint8[BUFFER_SIZE];
                this.connection.receive(msgbuf);
                switch (msgbuf[0]) {
                    case Messages.INIT:
                        var msg = (string)msgbuf;
                        var content = msg.substring(1);
                        var init = Init.deserialize(content);
                        this.handle_init(init);
                        message("Got init from server");
                        break;
                    case Messages.UPDATE:
                        var msg = (string)msgbuf;
                        var content = msg.substring(1);
                        var session = (Session)Json.gobject_from_data(typeof(Session), content);
                        this.handle_update(session);
                        message("Got update");
                        break;
                    default:
                        break;
                }
            }
            GLib.Idle.add(()=>{
                this.disconnected();
                return false;
            });
            return 0;
        }

        ~Client () {
            this.connection.close();
            GLib.Idle.add(()=>{
                this.disconnected();
            });
        }

        private void handle_update(Session s) {
            lock (this.sessions) {
                int idx = 0;
                foreach (var session in this.sessions) {
                    if (s.id == session.id) {
                        this.sessions.remove(session);
                        break;
                    }
                    idx++;
                }
                this.sessions.insert(s, idx);
            }
            GLib.Idle.add(()=>{
                this.sessions_changed(this.sessions);
                return false;
            });
        }

        private void handle_init(Init init) {
            lock (this.sessions) {
                foreach (var session in init.sessions) {
                    this.sessions.append(session);
                }
            }
            lock (this.locations) {
                foreach (var location in init.locations) {
                    this.locations.append(location);
                }
            }
            GLib.Idle.add(()=>{
                this.connected();
                this.sessions_changed(this.sessions);
                this.locations_changed(this.locations);
                return false;
            });
        }

        /**
         * This signal is being triggered whenever the server
         * notifies the client about changes to its sessions
         */
        public signal void sessions_changed(List<Session> sessions);

        /**
         * This signal is being triggered when there are changes
         * to the locations available on the server
         */
        public signal void locations_changed(List<string> locations);

        /**
         * This signal is being triggered when the connection
         * has been closed
         */
        public signal void disconnected();

        /**
         * This signal is being triggered when the connection
         * handshake has been completed
         */
        public signal void connected();
    }
}
